/*
 * Copyright 2011 The Rusted Roof, LLC.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.proteusframework.platformservice.persistence.messagebean;

import org.proteusframework.core.api.model.INamespace;
import org.proteusframework.core.base.Namespace;
import org.proteusframework.core.util.Assert;
import org.proteusframework.platformservice.persistence.api.messagebean.IMessageBeanDescriptor;
import org.proteusframework.platformservice.persistence.api.messagebean.IMessageBeanIndex;
import org.proteusframework.platformservice.persistence.api.messagebean.IMessageBeanProperty;

import java.util.ArrayList;
import java.util.List;

/**
 * Mandatory builder for creating instances of {@link IMessageBeanDescriptor}.
 *
 * @author Tacoma Four
 */
public final class MessageBeanBuilder
{
    private final MessageBeanDescriptor descriptor;
    private final INamespace namespace;

    public MessageBeanBuilder(INamespace namespace)
    {
        Assert.parameterNotNull(namespace, "Parameter 'namespace' must not be null");
        Assert.parameterNotNull(namespace.getFamily(), "Parameter 'namespace' must declare a non-null family");
        Assert.parameterNotNull(namespace.getId(), "Parameter 'namespace' must declare a non-null id");

        this.namespace = namespace;
        descriptor = new MessageBeanDescriptor(namespace);
    }

    /**
     * Creates an instance of a builder that creates an {@link IMessageBeanDescriptor} under the given package and
     * class name. The package and class <b>must</b> map to an actual
     * Java POJO that follows the JavaBean getter/setter standards for accessing class properties.
     * <p/>
     * The POJO <b>must</b> include a no-arg constructor.
     *
     * @param packageName Package name that maps to the actual package name of the POJO being mapped. The mapping must
     *                    be exactly the same package name as the actual bean, as the package name will be concatenated
     *                    with the class name and used within a <code>Class.forName(clazz)</code> call.
     * @param className   Class name that maps to the actual class name of the POJO being mapped. The mapping must
     *                    be exactly the same package name as the actual bean, as the package name will be concatenated
     *                    with the class name and used within a <code>Class.forName(clazz)</code> call.
     */
    public MessageBeanBuilder(String packageName, String className)
    {
        this(packageName, className, null);
    }

    /**
     * Creates an instance of a builder that creates an {@link IMessageBeanDescriptor} under the given package and
     * class name, with an optional (may be null) logical name. The package and class <b>must</b> map to an actual
     * Java POJO that follows the JavaBean getter/setter standards for accessing class properties.
     * <p/>
     * The POJO <b>must</b> include a no-arg constructor.
     *
     * @param packageName Package name that maps to the actual package name of the POJO being mapped
     * @param className   Class name that maps to the actual class name of the POJO being mapped
     * @param logicalName Optional logical named used to identify the message bean, or null if no logical name is needed
     */
    public MessageBeanBuilder(String packageName, String className, String logicalName)
    {
        this.namespace = new Namespace(packageName, className, logicalName);
        descriptor = new MessageBeanDescriptor(namespace);
    }

    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field is assumed to contain non-unique values, and the field may contain a null value. The data type
     * must be one that does not require any length definition, e.g. a {@link DataType#BooleanType},
     * {@link DataType#IntegerType}, etc.
     *
     * @param propertyName Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType     Data type
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType);
        descriptor.addProperty(field);
        return this;
    }

    public MessageBeanBuilder mapProperty(String propertyName,
                                          Class<? extends Enum> actualEnum)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, DataType.EnumType);
        field.getMetadata().setProperty("Class.forName", actualEnum.getCanonicalName());
        descriptor.addProperty(field);
        return this;
    }

    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field is assumed to contain non-unique values, and the field may contain a null value. The data type
     * must be one that requires a maximum length definition, e.g. a {@link DataType#StringType},
     * {@link DataType#ByteArrayType}, etc.
     *
     * @param propertyName Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType     Data type
     * @param length       Maximum field length
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType,
                                          int length)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType, length);
        descriptor.addProperty(field);
        return this;
    }

    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field is assumed to contain non-unique values, and the field may contain a null value. The data type
     * must be one that requires a maximum length definition, e.g. a {@link DataType#StringType},
     * {@link DataType#ByteArrayType}, etc.
     *
     * @param propertyName Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType     Data type
     * @param length       Maximum field length
     * @param precision    Maximum field precision
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType,
                                          int length,
                                          int precision)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType, length, precision);
        descriptor.addProperty(field);
        return this;
    }

    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field may or may not contain non-unique values, and the field must not support null values. The data
     * type must be one that does not require any length definition, e.g. a {@link DataType#BooleanType},
     * {@link DataType#IntegerType}, etc.
     *
     * @param propertyName     Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType         Data type
     * @param primaryKeyMember Flag that indicates if the property can be considered unique within the message domain
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType,
                                          boolean primaryKeyMember)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType, primaryKeyMember);
        descriptor.addProperty(field);
        return this;
    }

    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field may or may not contain non-unique values, and the field must not support null values. The data
     * type must be one that requires a maximum length definition, e.g. a {@link DataType#StringType},
     * {@link DataType#ByteArrayType}, etc.
     *
     * @param propertyName     Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType         Data type
     * @param length           Maximum field length
     * @param primaryKeyMember Flag that indicates if the property can be considered unique within the message domain
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType,
                                          int length,
                                          boolean primaryKeyMember)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType, length, primaryKeyMember);
        descriptor.addProperty(field);
        return this;
    }

    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field may or may not contain non-unique values, and the field must not support null values. The data
     * type must be one that requires a maximum length definition, e.g. a {@link DataType#StringType},
     * {@link DataType#ByteArrayType}, etc.
     *
     * @param propertyName     Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType         Data type
     * @param length           Maximum field length
     * @param precision        Maximum field precision
     * @param primaryKeyMember Flag that indicates if the property can be considered unique within the message domain
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType,
                                          int length,
                                          int precision,
                                          boolean primaryKeyMember)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType, length, precision, primaryKeyMember);
        descriptor.addProperty(field);
        return this;
    }


    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field may or may not contain non-unique values, and the field may or may not support null values.
     * The data type must be one that does not require any length definition, e.g. a {@link DataType#BooleanType},
     * {@link DataType#IntegerType}, etc.     *
     *
     * @param propertyName     Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType         Data type
     * @param primaryKeyMember Flag that indicates if the property can be considered unique within the message domain
     * @param nullableFlag     Flag that indicates if the property can support null values
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType,
                                          boolean primaryKeyMember,
                                          boolean nullableFlag)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType, primaryKeyMember, nullableFlag);
        descriptor.addProperty(field);
        return this;
    }

    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field may or may not contain non-unique values, and the field may or may not support null values.
     * The data type must be one that requires a maximum length definition, e.g. a {@link DataType#StringType},
     * {@link DataType#ByteArrayType}, etc.     *
     *
     * @param propertyName     Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType         Data type
     * @param length           Maximum field length
     * @param primaryKeyMember Flag that indicates if the property can be considered unique within the message domain
     * @param nullableFlag     Flag that indicates if the property can support null values
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType,
                                          int length,
                                          boolean primaryKeyMember,
                                          boolean nullableFlag)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType, length, primaryKeyMember, nullableFlag);
        descriptor.addProperty(field);
        return this;
    }

    /**
     * Maps a field with the given property name. The property name must map to a standard set of JavaBean getter/setter
     * methods, the field may or may not contain non-unique values, and the field may or may not support null values.
     * The data type must be one that requires a maximum length definition, e.g. a {@link DataType#StringType},
     * {@link DataType#ByteArrayType}, etc.     *
     *
     * @param propertyName     Property name that must have public getter/setter methods defined using JavaBean nomenclature
     * @param dataType         Data type
     * @param length           Maximum field length
     * @param precision        Maximum field precision
     * @param primaryKeyMember Flag that indicates if the property can be considered unique within the message domain
     * @param nullableFlag     Flag that indicates if the property can support null values
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          DataType dataType,
                                          int length,
                                          int precision,
                                          boolean primaryKeyMember,
                                          boolean nullableFlag)
    {
        MessageBeanProperty field = new MessageBeanProperty(namespace.getFamily(), propertyName, dataType, length, precision, primaryKeyMember, nullableFlag);
        descriptor.addProperty(field);
        return this;
    }

    /**
     * Maps arbitrary metadata to a property previously mapped to this {@link MessageBeanBuilder} instance. The
     * consumption of the metadata is defined by specific concrete persistence service implementations.
     *
     * @param propertyName POJO property
     * @param key          metadata key
     * @param value        metadata value
     * @return Builder
     */
    public MessageBeanBuilder mapProperty(String propertyName,
                                          String key,
                                          String value)
    {
        MessageBeanProperty messageField = descriptor.getProperty(propertyName);
        if (null == messageField)
        {
            throw new IllegalArgumentException("No field defined with the name " + propertyName);
        }

        messageField.getMetadata().setProperty(key, value);
        return this;
    }

    /**
     * Maps an arbitrary description to a specific property.
     *
     * @param propertyName POJO property
     * @param description  Description
     * @return Builder
     */
    public MessageBeanBuilder setPropertyDescription(String propertyName,
                                                     String description)
    {
        MessageBeanProperty messageField = descriptor.getProperty(propertyName);
        if (null == messageField)
        {
            throw new IllegalArgumentException("No field defined with the name " + propertyName);
        }

        messageField.setDescription(description);
        return this;
    }

    /**
     * Maps an arbitrary description to a specific property.
     *
     * @param propertyName  POJO property
     * @param propertyKey   Property key
     * @param propertyValue Property value
     * @return Builder
     */
    public MessageBeanBuilder addPropertyMetadata(String propertyName,
                                                  String propertyKey,
                                                  String propertyValue)
    {
        MessageBeanProperty messageField = descriptor.getProperty(propertyName);
        if (null == messageField)
        {
            throw new IllegalArgumentException("No field defined with the name " + propertyName);
        }

        messageField.getMetadata().setProperty(propertyKey, propertyValue);
        return this;
    }

    /**
     * Maps an arbitrary description to the message bean being mapped by this instance of
     * <code>MessageBeanBuilder</code>.
     *
     * @param description Description
     * @return Builder
     */
    public MessageBeanBuilder setDescription(String description)
    {
        descriptor.setDescription(description);
        return this;
    }

    /**
     * Maps arbitrary metadata to a bean being mapped to this {@link MessageBeanBuilder} instance. The
     * consumption of the metadata is defined by specific concrete persistence service implementations.
     *
     * @param key   metadata key
     * @param value metadata value
     * @return Builder
     */
    public MessageBeanBuilder addMetadata(String key, String value)
    {
        descriptor.getMetadata().setProperty(key, value);
        return this;
    }

    /**
     * Maps an index to a collection of properties previously mapped to this <code>MessageBeanBuilder</code> instance.
     * The handling of the index is defined by specific concrete persistence service implementations. Some
     * implementations may not support index definitions.
     *
     * @param indexName        Physical name used to represent the index definition
     * @param primaryKeyMember Flag that indicates if the collection of property names can be treated by the persistence
     *                         service as a unique index
     * @param propertyName     One or property names previously mapped to this <code>MessageBeanBuilder</code> instance.
     * @return Builder
     */
    public MessageBeanBuilder defineIndex(String indexName, boolean primaryKeyMember, String... propertyName)
    {
        Assert.parameterNotNull(indexName, "Parameter 'indexName' must not be null");
        Assert.parameterNotNull(propertyName, "Parameter 'fieldName' must not be null");

        List<IMessageBeanProperty> fields = new ArrayList<IMessageBeanProperty>();

        for (String field : propertyName)
        {
            IMessageBeanProperty messageField = descriptor.getProperty(field);
            if (null == messageField)
            {
                throw new IllegalArgumentException("No field defined with the name " + field);
            }

            fields.add(messageField);
        }

        IMessageBeanIndex index = new MessageBeanIndex(namespace.getFamily(),
                indexName,
                primaryKeyMember,
                fields.toArray(new IMessageBeanProperty[fields.size()]));

        descriptor.addIndex(index);

        return this;
    }


    /**
     * Maps an interface that the message bean supports. Each concrete persistence service implementation will define
     * what level of interface support it offers. The idea is that some persistence service implementations can accept
     * an interface name during persistence operations in lieu of the physical POJO class that was used during the
     * mapping process. Thus, multiple disparate concrete POJO types can be safely persisted under the auspices of an
     * interface, saving the developer from having to map say 10 concrete POJOs that all have the same signature set.
     *
     * @param interfaceName Interface name
     * @return Builder
     */
    public MessageBeanBuilder declareInterfaceSupport(Class<?> interfaceName)
    {
        Assert.parameterNotNull(interfaceName, "Parameter 'interfaceName' must not be null");
        descriptor.addInterface(interfaceName);
        return this;
    }

    public IMessageBeanDescriptor build()
    {
        return descriptor;
    }
}
